module nazo.gettext;
/**
 * APIは異なるけどgettextのような感じ。
 * Linuxでしか動かないかも。
 * History:
 *          0.0.0.1 バージョン付け開始
 * Authors: Nazo
 * Version: 0.0.0.1
 * License: Public Domain
 */
import nazo.env, std.file, std.string, std.stdint, std.stdio, std.intrinsic;
class LocaleFileNotFoundException:Exception{
  this(string msg){
    super("LocaleFileNotFoundException: "~msg);
  }
}
class InvalidLocaleFileException:Exception{
  this(string msg){
    super("InvalidLocaleFileException: "~msg);
  }
}

///moを使ってローカライズします。
class Localizer{
  private string localeName;
  private string filePath;
  private string[ubyte[]] texttable;
  private bool useHash=false;
  ///現在のロケール、実行ファイル名で初期化します。
  this(){
    this(Env.get("_").split("/")[$-1]);
  }
  ///現在のロケールで初期化します。
  ///bugs: 環境変数の順序がこれであってるかが不明。
  this(string fileName){
    if(Env.get("LANG")=="C")return;//正常終了
    if(Env.get("LANG").length>0){
      if(!initialize(Env.get("LANG").split(".")[0],fileName))throw new LocaleFileNotFoundException("");
      return;
    }
    foreach(string lang;Env.get("LANGUAGE").split(";")){
      if(initialize(lang,fileName))return;
    }
    if(true)throw new LocaleFileNotFoundException("");//XXX: dmdの警告ありで何故かエラーになるので回避
  }
  ///初期化します。
  this(string localeName,string fileName){
    if(!initialize(localeName,fileName))
      throw new LocaleFileNotFoundException("");
  }
  ///値を返します。
  string getText(string id){
    ubyte[] key;
    if(useHash){
      key=_toHash(id);
    }else{
      key=cast(ubyte[])id;
    }
//    writefln("%s:%s",key in texttable,key);
    return (key in texttable)?texttable[key]:id;
  }
  //"実行パス/locale/ロケール名/ファイル名"で初期化します。
  //unix系だと存在しなかったら"/usr/share/locale/ロケール名/LC_MESSAGES/ファイル名"辺りを探すはず。
  //成功したらtrueを失敗したらfalseを返します。
  private bool initialize(string localeName,string fileName){
    filePath=Env.get("_").split("/")[0..$-1].join("/")~"/locale/"~localeName~"/"~fileName~".mo";
    if(exists(filePath))return initialize(filePath);
    version(UNIX){
      filePath="/usr/share/locale/"~localeName~"/LC_MESSAGES/"~fileName~".mo";
      if(exists(filePath))return initialize(filePath);
    }
    if(localeName.find("_")!=-1)//ja_JP等の場合jaも探す
      return initialize(localeName.split("_")[0],fileName);
    if(localeName.find("@")!=-1)//en@quot等の場合enも探す
      return initialize(localeName.split("_")[0],fileName);
    return false;
  }
  //初期化
  //See: http://ring.atr.jp/archives/doc/gnu-info-j/gettext/gettext-ja.html
  //See: /gettext-0.14.5/gettext-tools/lib/
  private bool initialize(string filePath){
    ubyte[] data=cast(ubyte[])read(filePath);
    bool isLittleEndian;
    switch((cast(uint[])data[0..4].reverse)[0]){//XXX: ubyte[]のままだとswitch使えね
      case 0x950412de://bigEndian
        isLittleEndian=false;
        break;
      case 0xde120495://littleEndian
        isLittleEndian=true;
        break;
      default:
        throw new InvalidLocaleFileException("Invalid Endian Header");
    }
    uint s(uint i){
      if(!isLittleEndian)
        return bswap(i);
      return i;
    }
    if((cast(uint[])data[4..8])[0]!=0)throw new InvalidLocaleFileException("Invalid Version or Not Supported This Version");
    ///Bugs: エンディアンを無視してる所がある
    uint N=s((cast(int[])data[8..12])[0]);//文字列領域の長さ
    uint O=s((cast(int[])data[12..16])[0]);//オリジナル文字列テーブルへのオフセット(オリジナルテーブルはソートされてるらしい)
    uint T=s((cast(int[])data[16..20])[0]);//翻訳文字列テーブルへのオフセット
    uint S=s((cast(int[])data[20..24])[0]);//ハッシュテーブルのサイズ
    uint H=s((cast(int[])data[24..28])[0]);//ハッシュテーブルのオフセット
    uint[] originalPointerTable=cast(uint[])data[O..((N-1)*8)];
    uint[] translationPointerTable=cast(uint[])data[T..T + ((N-1)*8)];
    uint[] hashTable=cast(uint[])data[H..H+S*4];
    if(/** /S==0/ */true/**/){//ハッシュが無い場合
      for(uint i;i<originalPointerTable.length/2;i++){
        uint orgstart=originalPointerTable[i*2+1];
        uint orglen=originalPointerTable[i*2];
        uint trstart=translationPointerTable[i*2+1];
        uint trlen=translationPointerTable[i*2];
        texttable[data[orgstart..orgstart+orglen]]=cast(string)data[trstart..trstart+trlen];
      }
    }else{//ハッシュがある場合(ダブルハッシング)
///Bugs:ハッシュにバグがあるので使わないようにしている
///実装依存という仕様からして(ry
      useHash=true;
      for(uint i;i<translationPointerTable.length/2;i++){
        uint[] tmp;
        tmp~=hashTable[i];
        ubyte[] orghash=cast(ubyte[])tmp;
        uint trstart=translationPointerTable[i*2+1];
        uint trlen=translationPointerTable[i*2];
        texttable[orghash]=cast(string)data[trstart..trstart+trlen];
//try{writefln(texttable[orghash]~":%s",orghash);}catch(Exception e){};
      }
    }
    return true;
  }
  //ハッシュを求めます。
  //>正確なハッシングアルゴリズムはGNU gettextのコードに依存しています
  //との事らしいが詳しい事はよーわからん
  private ubyte[] _toHash(string hash){
    ubyte[] key=cast(ubyte[])hash;
    //See: <limits.h>
    const uint CHAR_BIT=8;
    size_t cnt;
    uint hval;
    cnt=0;
    hval=key.length;
    while(cnt<key.length){
      hval=(hval << 9) | (hval >> (uint.sizeof * CHAR_BIT - 9));
      hval+=key[cnt++];//多分あってるはず
    }
    uint[] tmp;
    tmp~= (hval != 0) ? hval : ~(cast(uint) 0);
    return cast(ubyte[])tmp;
  }
}

Localizer loc;
class NotInitializedException:Exception{
  this(string msg){
    super("NotInitializedException: "~msg);
  }
}

void setLocalizer(Localizer l){
  loc=l;
}

string _(string id){
  if(loc is null)throw new NotInitializedException("");
  return loc.getText(id);
}

string N_(string id){
  return id;
}
